// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qwasmanimationdriver_p.h"
#include "qwasmsuspendresumecontrol_p.h"

#include <emscripten/val.h>

QT_BEGIN_NAMESPACE


// QWasmAnimationDriver drives animations using requestAnimationFrame(). This
// ensures that animations are advanced in sync with frame update calls, which
// again are synced to the screen refresh rate.

namespace {
    constexpr int FallbackTimerInterval = 500;
}

QWasmAnimationDriver::QWasmAnimationDriver(QUnifiedTimer *)
    : QAnimationDriver(nullptr)
{
    connect(this, &QAnimationDriver::started, this, &QWasmAnimationDriver::start);
    connect(this, &QAnimationDriver::stopped, this, &QWasmAnimationDriver::stop);
}

QWasmAnimationDriver::~QWasmAnimationDriver()
{
    disconnect(this, &QAnimationDriver::started, this, &QWasmAnimationDriver::start);
    disconnect(this, &QAnimationDriver::stopped, this, &QWasmAnimationDriver::stop);

    if (m_animateCallbackHandle != 0)
        QWasmAnimationFrameMultiHandler::instance()->unregisterAnimateCallback(m_animateCallbackHandle);
}

qint64 QWasmAnimationDriver::elapsed() const
{
    return isRunning() ? qint64(m_currentTimestamp - m_startTimestamp) : 0;
}

double QWasmAnimationDriver::getCurrentTimeFromTimeline() const
{
    // Get the current timeline time, which is an equivalent time source to the
    // animation frame time source. According to the documentation this API
    // may be unavailable in various cases; check for null before accessing.
    emscripten::val document = emscripten::val::global("document");
    emscripten::val timeline = document["timeline"];
    if (!timeline.isNull() && !timeline.isUndefined()) {
        emscripten::val currentTime = timeline["currentTime"];
        if (!currentTime.isNull() && !currentTime.isUndefined())
            return currentTime.as<double>();
    }
    return 0;
}

void QWasmAnimationDriver::handleFallbackTimeout()
{
    if (!isRunning())
        return;

    // Get the current time from a timing source equivalent to the animation frame time
    double currentTime = getCurrentTimeFromTimeline();
    if (currentTime == 0)
        currentTime = m_currentTimestamp + FallbackTimerInterval;
    const double timeSinceLastFrame = currentTime - m_currentTimestamp;

    // Advance animations if animations are active but there has been no rcent animation
    // frame callback.
    if (timeSinceLastFrame > FallbackTimerInterval * 0.8) {
        m_currentTimestamp = currentTime;
        advance();
    }
}

void QWasmAnimationDriver::start()
{
    if (isRunning())
        return;

    // Set start timestamp to document.timeline.currentTime()
    m_startTimestamp = getCurrentTimeFromTimeline();
    m_currentTimestamp = m_startTimestamp;

    // Register animate callback
    m_animateCallbackHandle = QWasmAnimationFrameMultiHandler::instance()->registerAnimateCallback(
        [this](double timestamp) { handleAnimationFrame(timestamp); });

    // Start fallback timer to ensure animations advance even if animaton frame callbacks stop coming
    fallbackTimer.setInterval(FallbackTimerInterval);
    connect(&fallbackTimer, &QTimer::timeout, this, &QWasmAnimationDriver::handleFallbackTimeout);
    fallbackTimer.start();

    QAnimationDriver::start();
}

void QWasmAnimationDriver::stop()
{
    m_startTimestamp = 0;
    m_currentTimestamp = 0;

    // Stop and disconnect the fallback timer
    fallbackTimer.stop();
    disconnect(&fallbackTimer, &QTimer::timeout, this, &QWasmAnimationDriver::handleFallbackTimeout);

    // Deregister the animation frame callback
    if (m_animateCallbackHandle != 0) {
        QWasmAnimationFrameMultiHandler::instance()->unregisterAnimateCallback(m_animateCallbackHandle);
        m_animateCallbackHandle = 0;
    }

    QAnimationDriver::stop();
}

void QWasmAnimationDriver::handleAnimationFrame(double timestamp)
{
    if (!isRunning())
        return;

    m_currentTimestamp = timestamp;

    // Fall back to setting m_startTimestamp here in cases where currentTime
    // was not available in start() (gives 0 elapsed time for the first frame)
    if (m_startTimestamp == 0)
        m_startTimestamp = timestamp;

    advance();
}

QT_END_NAMESPACE
